﻿#region License

// Another free, open solution by someone who gets it.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Xml.Serialization;

using AutoMapper;

using Patterns.Mapping;
using Patterns.SafeCode;

namespace Patterns.Xml
{
	/// <summary>
	/// 	Defines ease-of-use extensions for Xml objects.
	/// </summary>
	public static class XmlExtensions
	{
		/// <summary>
		/// 	Merges the key-value pairs of the specified dictionary into the current xml as attributes.
		/// </summary>
		/// <param name = "xml">The XML.</param>
		/// <param name = "values">The values.</param>
		public static void MergeAttributes(this XElement xml, IDictionary<string, object> values)
		{
			//TODO: filter cases where attribute already exists
			xml.Add(values.Keys.Select(key => new XAttribute(key, values[key])));
		}

		/// <summary>
		/// 	Gets the value with the specified name.
		/// </summary>
		/// <param name = "xml">The XML.</param>
		/// <param name = "name">The name.</param>
		/// <returns></returns>
		public static string GetValue(this XElement xml, string name)
		{
			var isEqual = new Func<string, string, bool>(StringComparer.OrdinalIgnoreCase.Equals);
			IEnumerable<XAttribute> attributes = xml.Attributes().Where(attribute => isEqual(attribute.Name.LocalName, name));
			if (attributes.Any())
				return attributes.First().Value;

			IEnumerable<XElement> immediateChildren = xml.Elements().Where(element => isEqual(element.Name.LocalName, name));
			if (immediateChildren.Any())
				return immediateChildren.First().Value;

			return xml.Descendants().Where(child => isEqual(child.Name.LocalName, name))
				.Select(child => child.Value)
				.FirstOrDefault();
		}

		/// <summary>
		/// 	Converts the xml element to an object of the specified type by using xml serialization.
		/// </summary>
		/// <typeparam name = "T"></typeparam>
		/// <param name = "xml">The XML.</param>
		/// <returns></returns>
		public static T AsObject<T>(this XElement xml) where T : class
		{
			//TODO: merge this down into one method with an optional parameter
			return Try.Get(() => ConvertFromXml<T>(xml));
		}

		/// <summary>
		/// 	Converts the xml element to an object of the specified type by using xml serialization.
		/// </summary>
		/// <typeparam name = "T">The type of object to deserialize to.</typeparam>
		/// <param name = "xml">The XML.</param>
		/// <param name = "errorHandler">The error handler.</param>
		/// <returns></returns>
		public static T AsObject<T>(this XElement xml, Action<Exception> errorHandler) where T : class
		{
			//TODO: inline ConvertFromXml
			return Try.Get(() => ConvertFromXml<T>(xml), errorHandler);
		}

		/// <summary>
		/// 	Converts the object to an xml element by using xml serialization.
		/// </summary>
		/// <param name = "data">The data.</param>
		/// <param name = "errorHandler">The error handler.</param>
		/// <returns></returns>
		public static XElement AsXml(this object data, Action<Exception> errorHandler)
		{
			//TODO: inline ConvertToXml
			return Try.Get(() => ConvertToXml(data), errorHandler);
		}

		private static T ConvertFromXml<T>(XElement xml) where T : class
		{
			TypeMap map = Try.Get(() => Mapper.FindTypeMapFor(typeof (XElement), typeof (T)));
			if (map == null) Mapper.CreateMap<XElement, T>().ConvertFromXml();
			T result = Mapper.Map<XElement, T>(xml);
			return result;
		}

		private static XElement ConvertToXml(object data)
		{
			var serializer = new XmlSerializer(data.GetType());
			var writer = new StringWriter();
			serializer.Serialize(writer, data);
			writer.Flush();
			return XElement.Parse(writer.ToString());
		}
	}
}